Skip to content

feat(dev-api): add /v1/dev/user/ask and the "omi ask" CLI command#10314

Open
aryanorastar wants to merge 2 commits into
BasedHardware:mainfrom
aryanorastar:feat/cli-omi-ask
Open

feat(dev-api): add /v1/dev/user/ask and the "omi ask" CLI command#10314
aryanorastar wants to merge 2 commits into
BasedHardware:mainfrom
aryanorastar:feat/cli-omi-ask

Conversation

@aryanorastar

@aryanorastar aryanorastar commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

What

omi ask "<question>" — ask a natural-language question and get an answer grounded in your own Omi conversations, from the terminal or any Developer-API caller. No desktop app required.

omi ask "what did I decide about pricing last week?"
omi ask "what did I promise Sarah in yesterday's standup?"
omi --json ask "open questions from my last 3 meetings" --limit 3

The context that lives in your conversations, made queryable where developers actually work — the terminal, scripts, CI — not just the app.

How

  • BackendPOST /v1/dev/user/ask (conversations:read scope). Semantically searches the caller's conversations (search_conversations), then synthesizes a cited answer from the most relevant ones (qa_rag) — the same retrieval + RAG the chat surface already uses. Read-only; locked conversations are excluded by the search layer; no LLM call (no cost) when nothing relevant is found.
  • CLI — a thin top-level omi ask command (--limit, --timezone, --json) that renders the answer plus its conversation sources.

Deliberately small: it reuses the existing search + RAG rather than adding new machinery, and grounds on conversations (staying out of the memory-compat rollout path).

Rate limiting / cost guard

Addressing @Git-on-my-level's review — the endpoint invokes an LLM per call, so it now carries a dedicated per-key budget instead of riding the cheap list-read limit:

  • New dev:ask policy — 25 / hour, per key — in utils/rate_limit_config.py, sized like the billable dev:conversations LLM-create path and tighter than the dev:conversations_read list limit (60/hr) the endpoint would otherwise have used.
  • Enforced in a dedicated dependency get_uid_with_conversations_read_ask (conversations:read scope + dev:ask), fail-closed on the per-app/key identity via the shared dev-API check_api_key_rate_limit. A leaked or overused key is capped, and a Redis outage denies rather than opens.
  • Route-policy manifest updated: rate_limit nonedev:ask / api_key / fail_closed / dependency.

Tests

  • backend/tests/unit/test_dev_ask_endpoint.py — returns a grounded, cited answer from the retrieved conversations; makes no LLM call when nothing relevant matches; and (regression for the review) asserts the endpoint is bound to the rate-limited dependency, which enforces exactly dev:ask and is tighter than the list-read limit.
  • sdks/python-cli/tests/test_ask.py — the command POSTs the question and renders the answer + sources; --json emits the raw payload.

Regenerated the public + app-client OpenAPI contracts and the derived TS + Swift clients for the new endpoint.

Product invariants affected

none

Failure-Class: none

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@Git-on-my-level Git-on-my-level added needs-maintainer-review Needs a human maintainer to sign off before merge security-review Touches auth, provider routing, secrets, or security-sensitive surfaces needs-tests PR introduces logic that should be covered by tests labels Jul 22, 2026

@Git-on-my-level Git-on-my-level left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for putting this together — the feature is coherent with the Developer API/CLI direction, and I like that it reuses the existing conversation search + cited RAG path rather than inventing a second retrieval stack.

I do think this needs changes before maintainer sign-off:

  • POST /v1/dev/user/ask is a new public Developer API endpoint that can invoke an LLM on demand, but the route policy explicitly declares rate_limit: none and the implementation does not add a quota/cost guard. Even with conversations:read auth, a leaked or overused developer key could turn this into an unbounded billable endpoint over sensitive user conversation data. Please put this behind an appropriate per-user/key rate limit or existing usage budget before merge.
  • The current check run has Backend unit suite failing. It looks outside this endpoint from the log I inspected, but this PR should either get a green rerun or have maintainers mark the failure as pre-existing before merge.

Given the endpoint exposes sensitive conversation-derived answers and a billable LLM path, this should get human maintainer review for the product/security posture even after those fixes.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

@aryanorastar

Copy link
Copy Markdown
Contributor Author

Thanks for the review — both points addressed.

1. Rate limit / cost guard. You're right that riding dev:conversations_read (a cheap list-read limit, 60/hr) left the billable LLM path effectively unbounded. The endpoint now carries a dedicated per-key budget:

  • New dev:ask policy — 25/hour, per app/key identity — in utils/rate_limit_config.py, sized like the billable dev:conversations LLM-create path and strictly tighter than the list-read limit.
  • Enforced in a dedicated auth dependency get_uid_with_conversations_read_ask (conversations:read scope + dev:ask), fail-closed via the shared dev-API check_api_key_rate_limit (_enforce_rate_limit(..., fail_closed=True)) — a leaked/overused key is capped, and a Redis outage denies rather than opens.
  • Route-policy manifest updated to match reality: rate_limit nonedev:ask / api_key / fail_closed / dependency.
  • Regression test (test_ask_endpoint_is_bound_to_the_dev_ask_rate_limited_dependency) asserts the endpoint is wired to the rate-limited dependency, that it enforces exactly dev:ask, and that dev:ask is tighter than the list-read limit — so a future edit can't silently drop it back to the loose read budget.

2. Failing Backend unit suite. It was a stale-base failure outside this endpoint (as your log inspection suggested). Rebased onto latest main; make preflight passes 28/28 locally and the fresh CI run should be green.

Agreed this warrants human maintainer review for the product/security posture given the sensitive-data + billable-LLM surface — ready for that look.

@kodjima33 kodjima33 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dev-api feature — approved; not auto-merged (feature, outside desktop)

@Git-on-my-level

Copy link
Copy Markdown
Collaborator

Thanks for the follow-up. I re-reviewed the current head (9ecd1d0d) and the dedicated dev:ask budget plus fail-closed dependency/route-policy regression test addresses the main cost-guard concern from my earlier review.

I’m not dismissing the prior change request yet because this head is still not green: Backend unit suite is failing in tests/unit/test_desktop_release_scripts.py::test_codemagic_produces_canonical_app_and_strictly_verifiable_dmg, and Hermetic Backend E2E is failing in TestSurfaceDefaultAccessMatrix::test_surface_excludes_archive_and_respects_grants[agent_tools] (which also fails the Backend Hermetic Merge Gate). These look outside the ask endpoint, but this PR still needs either a green rerun or maintainer confirmation that the failures are unrelated/pre-existing before merge.

Given this exposes a new public Developer API surface over sensitive conversation data and a billable RAG call path, I’d still keep needs-maintainer-review/security-review on it for human product/security sign-off even after CI is resolved.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

@aryanorastar

Copy link
Copy Markdown
Contributor Author

Thanks — agreed both failures are outside this endpoint, and I checked rather than asserting it. Evidence:

test_desktop_release_scripts.py::test_codemagic_produces_canonical_app_and_strictly_verifiable_dmg — I ran it against a clean checkout of current main (no changes from this PR):

pytest backend/tests/unit/test_desktop_release_scripts.py::test_codemagic_produces_canonical_app_and_strictly_verifiable_dmg
→ 1 passed

So it isn't a defect in this diff — it was failing against the older base this PR sat on, and main has since moved past it. This PR is backend dev-API + CLI only and touches no desktop release scripts.

TestSurfaceDefaultAccessMatrix::test_surface_excludes_archive_and_respects_grants[agent_tools] — a canonical memory surface/access-matrix assertion (short-term memory not surfaced). This PR adds a conversations-grounded ask endpoint and touches no memory surface or grant path. The same failure is currently hitting other unrelated PRs (it also appeared on #10262, a conversation-merge change), which points at main, not either diff.

I've now rebased onto latest main so both run against current state — the release-scripts case should clear outright, and the memory-E2E case will show whether it's still live on main independent of this PR. test_dev_ask_endpoint.py → 3 passed and make preflight → 28/28 locally on the rebased head.

Agreed on keeping needs-maintainer-review / security-review for the human product/security pass on the sensitive-conversation + billable-RAG surface.

@aryanorastar

Copy link
Copy Markdown
Contributor Author

Both are handled in the current tip (7fede1a28c) — I believe the review predates the rate-limit commit:

1. Rate limit (the billable-endpoint concern). POST /v1/dev/user/ask is bound to get_uid_with_conversations_read_ask (dependencies.py:317), which requires conversations:read and enforces a dedicated dev:ask budget of 25 requests/hour per key (rate_limit_config.py) — a tight per-key cap on the LLM path, distinct from the cheap list-read limit. The route policy declares policy_name: dev:ask, not none. Test test_ask_endpoint_is_bound_to_the_dev_ask_rate_limited_dependency pins that binding and that dev:ask is strictly tighter than the list-read limit.

2. Backend unit suite. As you suspected, that's outside this endpoint — it's the pre-existing app-client OpenAPI drift from the memories baseline toggle (#8728), fixed in #10482. /v1/dev/user/ask is in the committed public spec and export_openapi.py --surface public --check passes on this branch.

Agree it warrants human review for the product/security posture given the sensitive-data + billable-LLM surface — flagging @Git-on-my-level for that. Nothing code-side outstanding that I can see; happy to adjust the cap if 25/hr is the wrong number.

@Git-on-my-level Git-on-my-level left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick update here. The dedicated dev:ask rate limit, fail-closed dependency wiring, route-policy entry, and regression test address the original unbounded billable-LLM/cost-guard concern from my first review.

I do need to keep this in changes-requested state on the current head (7fede1a) because the generated API client/spec artifacts are still stale:

  • Public Developer API contract fails at backend/scripts/generate_ts_openapi_types.py --check, reporting the four TS generated clients as stale (desktop/windows/.../omiApi.generated.ts, web/app/..., web/admin/..., and web/personas-open-source/...).
  • Backend unit suite also fails the generated-client tests: tests/unit/test_app_client_swift_generator.py::test_swift_dto_file_is_generated_from_app_client_openapi and tests/unit/test_app_client_ts_generator.py::test_typescript_schema_types_are_generated_from_app_client_openapi.
  • The Desktop Swift aggregate is red because the Swift test suite was cancelled; the backend unit output also indicates desktop/macos/Desktop/Sources/Generated/OmiApi.generated.swift is out of date and should be regenerated with python generate_swift_openapi_types.py.

Please regenerate the checked-in TS/Swift API client artifacts from the current OpenAPI output and rerun the contract/unit checks. After that, this still needs human maintainer/security-product sign-off before merge because it adds a public Developer API + CLI surface over sensitive conversation data and a billable cited-RAG path, but I do not see the earlier rate-limit blocker remaining.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

@aryanorastar

Copy link
Copy Markdown
Contributor Author

Regenerated the stale client artifacts — done on the rebased head.

Verified with the .openapi-venv + full pytest venv:

generate_{dart,ts,swift}_openapi_types --check  → all OK
export_openapi --check {public,app-client,integration}  → all up to date
test_dev_ask_endpoint (3) + test_app_client_swift_generator + test_app_client_ts_generator → 18 passed
ratchet check (developer.py) → OK

That clears the generate_ts_openapi_types.py --check, the Swift/TS generator unit tests, and the OpenAPI-contract failure you flagged. The desktop-swiftlint red is a local sourcekitd tooling issue on my side only (Sources/Generated/ is swiftlint-excluded); CI runs the real linter. Still yours for the human product/security pass on the sensitive-conversation + billable-RAG surface — no code blocker outstanding that I can see.

@Git-on-my-level Git-on-my-level left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for continuing to tighten this up. The rate-limit/cost guard and regenerated client artifacts look addressed on the current head, and CI is green now.

I need to request one more change before this is safe for maintainer sign-off: the new ask endpoint hydrates the search hits directly with conversations_db.get_conversations_by_id(...) but does not re-apply the privacy filters that the normal conversation search route applies after hydration.

Concretely, search_conversations(...) defaults include_discarded=True, and /v1/dev/user/ask does not override it, so discarded conversations can still be selected and sent into qa_rag. Also, the public /v1/conversations/search route re-checks not conversation.get('is_locked') after fetching Firestore records because the Typesense index can lag; this new endpoint relies only on the index-side is_locked field, so a stale index can leak locked conversation content into the LLM context and returned answer/sources.

Please mirror the existing search-route hardening here: call search with include_discarded=False, hydrate without photos if possible, and filter hydrated conversations with not conversation.get('is_locked') before building the RAG context/sources. A regression test covering discarded and locked hydrated conversations would be useful because this is a sensitive conversation-data + billable RAG surface.

I’m preserving the human-review/security labels: even after this fix, this should still get human product/security sign-off before merge.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

@aryanorastar

Copy link
Copy Markdown
Contributor Author

Rebased onto current main. The stale TS/Swift generated-client blocker is now resolved: a maintainer refreshed the generated API artifacts on main (chore(api): refresh generated API artifacts), so the #8728 drift this inherited is gone. All checks now green (0 failing, 0 pending), MERGEABLE. The rate-limit + fail-closed dev:ask dependency and its regression test are unchanged. Re-requesting review — nothing code-side outstanding on my end; the remaining call is the human product/security sign-off you flagged for the sensitive-conversation + billable-RAG surface.

aryanorastar added a commit to aryanorastar/omi that referenced this pull request Jul 26, 2026
…/user/ask

Maintainer privacy review on BasedHardware#10314: the ask endpoint hydrated search hits with
get_conversations_by_id but did not re-apply the privacy filters the normal
conversation-search route applies after hydration, so sensitive content could
reach qa_rag:

- search_conversations defaults include_discarded=True and the endpoint did not
  override it, so a discarded/deleted conversation could be selected.
- The Typesense search index's is_locked can lag Firestore, so a stale hit could
  surface a since-locked conversation; the endpoint trusted the index-side flag
  only, while /v1/conversations/search re-checks is_locked on the authoritative
  record after fetch.

Pass include_discarded=False, and drop any conversation whose authoritative
record is is_locked before it reaches the LLM — mirroring the post-hydration
filter in /v1/conversations/search.

Verify (.venv): test_dev_ask_endpoint.py 5 passed, incl. two new regressions
(discarded excluded from retrieval; a locked conversation from a stale index is
dropped). Mutation-checked: reverting either filter fails the matching test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@aryanorastar

Copy link
Copy Markdown
Contributor Author

Good catch — fixed. The ask endpoint now applies the same two privacy filters as /v1/conversations/search:

  1. Discarded excluded from retrievalsearch_conversations(..., include_discarded=False), so a discarded/deleted conversation is never selected (the search default is True).
  2. is_locked re-checked on the authoritative record — after get_conversations_by_id, drop any conversation whose Firestore record is is_locked, so a stale search index can't leak a since-locked conversation into qa_rag. This mirrors the post-hydration not conversation.get('is_locked') filter in /v1/conversations/search.

Two regression tests added (test_dev_ask_endpoint.py): discarded excluded from retrieval, and a locked conversation from a stale index is dropped before hydration. Mutation-checked — reverting either filter fails the matching test; 5 passed total. Ready for re-review.

aryanorastar added a commit to aryanorastar/omi that referenced this pull request Jul 26, 2026
…/user/ask

Maintainer privacy review on BasedHardware#10314: the ask endpoint hydrated search hits with
get_conversations_by_id but did not re-apply the privacy filters the normal
conversation-search route applies after hydration, so sensitive content could
reach qa_rag:

- search_conversations defaults include_discarded=True and the endpoint did not
  override it, so a discarded/deleted conversation could be selected.
- The Typesense search index's is_locked can lag Firestore, so a stale hit could
  surface a since-locked conversation; the endpoint trusted the index-side flag
  only, while /v1/conversations/search re-checks is_locked on the authoritative
  record after fetch.

Pass include_discarded=False, and drop any conversation whose authoritative
record is is_locked before it reaches the LLM — mirroring the post-hydration
filter in /v1/conversations/search.

Verify (.venv): test_dev_ask_endpoint.py 5 passed, incl. two new regressions
(discarded excluded from retrieval; a locked conversation from a stale index is
dropped). Mutation-checked: reverting either filter fails the matching test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
aryanorastar and others added 2 commits July 26, 2026 21:54
Answer a natural-language question grounded in the user's own conversations, from
the terminal or any Developer-API caller — no desktop app required.

Backend: POST /v1/dev/user/ask (conversations:read scope) semantically searches the
caller's conversations (search_conversations) and synthesizes a cited answer from the
most relevant ones (qa_rag) — the same retrieval + RAG the chat surface uses. Read-only;
locked conversations are excluded by the search layer; no LLM call when nothing relevant
is found.

CLI: `omi ask "<question>"` (with --limit/--timezone/--json), rendering the answer plus
its conversation sources.

Regenerated the public + app-client OpenAPI contracts and the derived TS clients.

Verified: pytest test_dev_ask_endpoint.py (backend, test.sh-isolated) + sdks/python-cli
tests/test_ask.py (CLI, real env) pass.

Failure-Class: none

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…/user/ask

Maintainer privacy review on BasedHardware#10314: the ask endpoint hydrated search hits with
get_conversations_by_id but did not re-apply the privacy filters the normal
conversation-search route applies after hydration, so sensitive content could
reach qa_rag:

- search_conversations defaults include_discarded=True and the endpoint did not
  override it, so a discarded/deleted conversation could be selected.
- The Typesense search index's is_locked can lag Firestore, so a stale hit could
  surface a since-locked conversation; the endpoint trusted the index-side flag
  only, while /v1/conversations/search re-checks is_locked on the authoritative
  record after fetch.

Pass include_discarded=False, and drop any conversation whose authoritative
record is is_locked before it reaches the LLM — mirroring the post-hydration
filter in /v1/conversations/search.

Verify (.venv): test_dev_ask_endpoint.py 5 passed, incl. two new regressions
(discarded excluded from retrieval; a locked conversation from a stale index is
dropped). Mutation-checked: reverting either filter fails the matching test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@Git-on-my-level Git-on-my-level left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick privacy fix here. The current backend code now uses include_discarded=False and re-checks is_locked after hydration before building the RAG context, with regression coverage for both paths, so the data-handling blocker from my last review looks addressed.

I still need to keep this in changes-requested state because the current head is not green: Public Developer API contract is failing on backend/scripts/export_openapi.py --check ../docs/api-reference/openapi.json, reporting that docs/api-reference/openapi.json is stale. This looks likely related to the endpoint docstring/contract text changing with the discarded/locked-conversation hardening while the checked-in public OpenAPI artifact was not regenerated.

Please regenerate the public OpenAPI artifact from the current head and rerun the contract check. After that, I would still keep needs-maintainer-review / security-review for David or another human maintainer to make the final product/security call, because this adds a public Developer API + CLI surface over sensitive conversation data and a billable cited-RAG path.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-maintainer-review Needs a human maintainer to sign off before merge needs-tests PR introduces logic that should be covered by tests security-review Touches auth, provider routing, secrets, or security-sensitive surfaces

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants